Skip to content

fix: recover from browser context crash without server restart#385

Open
whyyagswhy wants to merge 1 commit intostickerdaniel:mainfrom
whyyagswhy:fix/browser-context-crash-recovery
Open

fix: recover from browser context crash without server restart#385
whyyagswhy wants to merge 1 commit intostickerdaniel:mainfrom
whyyagswhy:fix/browser-context-crash-recovery

Conversation

@whyyagswhy
Copy link
Copy Markdown

Problem

When the Patchright browser context dies mid-operation, the MCP HTTP server
stays alive but every subsequent tool call fails with:

TargetClosedError: Page.evaluate: Target page, context or browser has been closed

Because the _browser singleton is still set (to the dead context),
get_or_create_browser() keeps returning it. The server appears healthy to
external monitors (HTTP 200) but cannot actually perform any LinkedIn operations.
The only recovery is a manual process restart.

Root cause

SequentialToolExecutionMiddleware.on_call_tool() is the single chokepoint for
all tool calls, but it has no handling for browser-level errors — only a finally
block for timing. TargetClosedError is also absent from error_handler.py.

Fix

In on_call_tool():

  1. Detect TargetClosedError (matched by the Playwright error message substring,
    which is stable across versions).
  2. Call close_browser() to reset the _browser singleton to None.
  3. Raise a ToolError with a clear "please retry" message.

On the next tool call, get_or_create_browser() finds _browser is None and
reinitializes from the persistent on-disk profile — no LinkedIn re-login
required
, since cookies are stored in the profile directory.

Behaviour after this fix

Scenario Before After
Browser context crashes All subsequent calls fail forever Crashing call returns "please retry"; next call succeeds
Time to recover Manual restart required ~5–10s (browser cold-start from cached profile)
LinkedIn session Unaffected Unaffected (cookies on disk)

Test

# Simulate a TargetClosedError in a tool call and verify:
# 1. close_browser() is called (browser singleton reset to None)
# 2. A ToolError with the expected message is raised
# 3. A subsequent call to get_or_create_browser() succeeds

Tested manually: caused a browser context crash (via close_session tool
mid-operation), confirmed the next tool call recovered automatically.

…dleware

When the Patchright browser context dies mid-operation (TargetClosedError),
the HTTP server stays alive and all subsequent tool calls fail with the same
error. The fix detects this condition in the middleware — the single chokepoint
for all tool calls — resets the browser singleton, and returns a descriptive
ToolError so the client knows to retry. The next call reinitializes the browser
from the persistent on-disk profile without requiring a new LinkedIn login.

Before this fix: every tool call after a browser crash returns TargetClosedError
indefinitely until the server process is manually restarted.

After this fix: the crashing call returns a clear "please retry" message; the
next call reinitializes the browser and succeeds (~5–10s cold start).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR adds automatic recovery from TargetClosedError (Patchright/Playwright browser context crashes) inside SequentialToolExecutionMiddleware.on_call_tool(). When a crash is detected, close_browser() is called to reset the _browser singleton so the next tool call reinitialises the browser from the on-disk profile without requiring a server restart or re-login. The recovery logic itself is sound and the singleton ordering guarantees correctness; the two remaining notes are minor robustness improvements (wrapped close call and type-based detection) that do not block the fix.

Confidence Score: 5/5

Safe to merge — recovery logic is correct and all remaining findings are P2 style suggestions.

The singleton is reset before any potentially-failing browser.close() call, so recovery is guaranteed regardless of whether close_browser() raises. Both open comments are hardening/style suggestions, not correctness defects.

No files require special attention; sequential_tool_middleware.py is the only changed file and its logic is correct.

Important Files Changed

Filename Overview
linkedin_mcp_server/sequential_tool_middleware.py Adds browser-crash detection and singleton reset in the tool-execution middleware; recovery path is correct but close_browser() can raise internally on a dead context and the detection relies solely on a substring match.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Middleware as SequentialToolMiddleware
    participant Tool as Tool Handler
    participant Browser as BrowserManager

    Client->>Middleware: on_call_tool(context)
    Middleware->>Middleware: acquire _lock
    Middleware->>Tool: call_next(context)
    Tool->>Browser: page operation
    Browser-->>Tool: TargetClosedError
    Tool-->>Middleware: raises TargetClosedError

    alt _is_browser_context_closed == True
        Middleware->>Middleware: log WARNING
        Middleware->>Browser: close_browser() sets _browser=None
        Middleware-->>Client: raise ToolError please retry
    else other exception
        Middleware-->>Client: re-raise original exception
    end

    Client->>Middleware: retry on_call_tool(context)
    Middleware->>Browser: get_or_create_browser() reinitialises from disk
    Browser-->>Middleware: fresh BrowserManager
    Middleware->>Tool: call_next(context)
    Tool-->>Client: success
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/sequential_tool_middleware.py
Line: 85-93

Comment:
**`close_browser()` may itself raise on a crashed context**

`close_browser()` resets the singleton first (safe), but then calls `await browser.close()` without a surrounding `try/except`. When the browser context is already dead, that call is likely to raise another Playwright error, which propagates out of `close_browser()` and is silently swallowed here at `DEBUG` level. Recovery still works because the singleton was already cleared, but the only observable signal is a DEBUG log rather than a WARNING. Consider logging at `WARNING` when the outer catch fires, or wrapping `browser.close()` inside `close_browser()` itself so the crash path is explicit.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: linkedin_mcp_server/sequential_tool_middleware.py
Line: 22-24

Comment:
**Substring match may miss localized or wrapped error messages**

`_is_browser_context_closed` converts the exception to a string and checks for a substring. If the Playwright / Patchright version in use ever wraps `TargetClosedError` inside another exception type (e.g. a transport or serialization wrapper), `str(exc)` may only contain the outer exception's message and the match will silently fail, leaving the server in the broken-singleton state. Checking `type(exc).__name__ == "TargetClosedError"` (or a similar type-based guard) as a primary condition, with the substring as a fallback, would be more robust.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: recover from browser context crash ..." | Re-trigger Greptile

Comment on lines +85 to +93
try:
# Lazy import avoids a circular dependency at module load time.
from linkedin_mcp_server.drivers.browser import close_browser
await close_browser()
except Exception:
logger.debug(
"close_browser() failed during crash recovery",
exc_info=True,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 close_browser() may itself raise on a crashed context

close_browser() resets the singleton first (safe), but then calls await browser.close() without a surrounding try/except. When the browser context is already dead, that call is likely to raise another Playwright error, which propagates out of close_browser() and is silently swallowed here at DEBUG level. Recovery still works because the singleton was already cleared, but the only observable signal is a DEBUG log rather than a WARNING. Consider logging at WARNING when the outer catch fires, or wrapping browser.close() inside close_browser() itself so the crash path is explicit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/sequential_tool_middleware.py
Line: 85-93

Comment:
**`close_browser()` may itself raise on a crashed context**

`close_browser()` resets the singleton first (safe), but then calls `await browser.close()` without a surrounding `try/except`. When the browser context is already dead, that call is likely to raise another Playwright error, which propagates out of `close_browser()` and is silently swallowed here at `DEBUG` level. Recovery still works because the singleton was already cleared, but the only observable signal is a DEBUG log rather than a WARNING. Consider logging at `WARNING` when the outer catch fires, or wrapping `browser.close()` inside `close_browser()` itself so the crash path is explicit.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +22 to +24
def _is_browser_context_closed(exc: Exception) -> bool:
"""Return True if *exc* indicates the Patchright browser context has died."""
return _BROWSER_CONTEXT_CLOSED in str(exc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Substring match may miss localized or wrapped error messages

_is_browser_context_closed converts the exception to a string and checks for a substring. If the Playwright / Patchright version in use ever wraps TargetClosedError inside another exception type (e.g. a transport or serialization wrapper), str(exc) may only contain the outer exception's message and the match will silently fail, leaving the server in the broken-singleton state. Checking type(exc).__name__ == "TargetClosedError" (or a similar type-based guard) as a primary condition, with the substring as a fallback, would be more robust.

Prompt To Fix With AI
This is a comment left during a code review.
Path: linkedin_mcp_server/sequential_tool_middleware.py
Line: 22-24

Comment:
**Substring match may miss localized or wrapped error messages**

`_is_browser_context_closed` converts the exception to a string and checks for a substring. If the Playwright / Patchright version in use ever wraps `TargetClosedError` inside another exception type (e.g. a transport or serialization wrapper), `str(exc)` may only contain the outer exception's message and the match will silently fail, leaving the server in the broken-singleton state. Checking `type(exc).__name__ == "TargetClosedError"` (or a similar type-based guard) as a primary condition, with the substring as a fallback, would be more robust.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant